package de.joergviola.mobj.client.storage;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

import com.google.code.gwt.storage.client.Storage;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.json.client.JSONNumber;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONParser;
import com.google.gwt.json.client.JSONString;
import com.google.gwt.json.client.JSONValue;
import com.google.gwt.user.client.rpc.AsyncCallback;

import de.joergviola.mobj.client.ITransaction;
import de.joergviola.mobj.shared.Entity;
import de.joergviola.mobj.shared.Filter;
import de.joergviola.mobj.shared.ObjectNotFoundException;
import de.joergviola.mobj.shared.Persistable;
import de.joergviola.mobj.shared.Query;
import de.joergviola.mobj.shared.Schema;
import de.joergviola.mobj.shared.UnknownEntityException;

public class StorageTransaction implements ITransaction {
	static final DateTimeFormat DATE_FORMAT = DateTimeFormat
			.getFormat("yyyy.MM.dd HH:mm:ss");

	private static final Logger log = Logger.getLogger("StorageTransaction");
	private final Storage storage;
	private final Schema schema;

	public StorageTransaction(Storage storage, Schema schema) {
		this.storage = storage;
		this.schema = schema;
	}

	<T extends Persistable> Object insertOrUpdate(T t)
			throws UnknownEntityException {
		Entity<T> entity = schema.getEntity(t);
		if (entity == null)
			throw new UnknownEntityException(t.getClass().getName());
		Object id = entity.getId(t);
		String key = entity.getEntityName() + "." + id;
		JSONObject json = toJSON(entity, t);
		storage.setItem(key, json.toString());
		return id;
	}

	@Override
	public <T extends Persistable> void insert(T t, AsyncCallback<T> callback) {
		log.info("Insert " + t);
		// set t.id
		try {
			insertOrUpdate(t);
		} catch (UnknownEntityException e) {
			callback.onFailure(e);
			return;
		}
		callback.onSuccess(t);
	}

	@Override
	public <T extends Persistable> void update(T t, AsyncCallback<Void> callback) {
		log.info("Update " + t);
		try {
			insertOrUpdate(t);
		} catch (UnknownEntityException e) {
			callback.onFailure(e);
			return;
		}
		callback.onSuccess(null);
	}

	private <T extends Persistable> JSONObject toJSON(Entity<T> entity, T t)
			throws UnknownEntityException {
		JSONObject json = new JSONObject();
		entity.getValues(t, new JSONEntityWriter(this, json));
		return json;
	}

	private <T extends Serializable> T fromJSON(Entity<T> entity,
			JSONObject json) throws ObjectNotFoundException,
			UnknownEntityException {
		return entity.getObject(new JSONEntityReader(this, json));
	}

	@Override
	public <T extends Persistable> void get(String className, Long id,
			AsyncCallback<T> callback) {
		log.info("Get " + className + ":" + id);
		try {
			Entity<T> entity = schema.getEntity(className);
			if (entity == null)
				throw new UnknownEntityException();
			String key = entity.getEntityName() + "." + id;
			String data = storage.getItem(key);
			if (data == null)
				throw new ObjectNotFoundException(id);
			JSONObject json = (JSONObject) JSONParser.parseStrict(data);
			T t = fromJSON(entity, json);
			callback.onSuccess(t);
		} catch (Throwable caught) {
			callback.onFailure(caught);
		}
	}

	@Override
	public <T extends Persistable> void get(String className, List<Long> ids,
			AsyncCallback<List<T>> callback) {
		log.info("Get " + className + ":" + ids);
		Entity<T> entity = schema.getEntity(className);
		ArrayList<T> result = new ArrayList<T>();
		for (Long id : ids) {
			String key = entity.getEntityName() + "." + id;
			String data = storage.getItem(key);
			if (data == null) {
				callback.onFailure(new ObjectNotFoundException(id));
				return;
			} else {
				JSONObject json = (JSONObject) JSONParser.parseStrict(data);
				T t;
				try {
					t = fromJSON(entity, json);
				} catch (ObjectNotFoundException e) {
					callback.onFailure(e);
					return;
				} catch (UnknownEntityException e) {
					callback.onFailure(e);
					return;
				}
				result.add(t);
			}
		}
		callback.onSuccess(result);
	}

	@Override
	public <T extends Persistable> void find(String className, Query query,
			AsyncCallback<List<T>> callback) {
		log.info("Find " + className + " where " + query);
		Entity<T> entity = schema.getEntity(className);
		if (entity == null) {
			callback.onFailure(new UnknownEntityException(
					"Entity not registered: " + className));
			return;
		}
		String entityName = entity.getEntityName();
		ArrayList<T> result = new ArrayList<T>();
		for (int i = 0; i < storage.getLength(); i++) {
			String key = storage.key(i);
			if (!key.startsWith(entityName))
				continue;
			String data = storage.getItem(key);
			JSONObject json = (JSONObject) JSONParser.parseStrict(data);
			if (matches(json, query)) {
				T t;
				try {
					t = fromJSON(entity, json);
				} catch (ObjectNotFoundException e) {
					callback.onFailure(e);
					return;
				} catch (UnknownEntityException e) {
					callback.onFailure(e);
					return;
				}
				result.add(t);
			}
		}
		log.info("Found " + className + " objects: " + result.size());
		callback.onSuccess(result);
	}

	private boolean matches(JSONObject json, Query query) {
		for (Filter filter : query.getFilter()) {
			JSONValue value = json.get(filter.getField());
			if (value == null && filter.getValue() != null)
				return false;
			JSONString s = value.isString();
			if (s != null) {
				if (!s.stringValue().equals(filter.getValue()))
					return false;
			}
			JSONNumber n = value.isNumber();
			if (n != null) {
				if (!new Long((long) n.doubleValue()).equals(filter.getValue()))
					return false;
			}
		}
		return true;
	}

	@Override
	public <T extends Persistable> void delete(T t, AsyncCallback<Void> callback) {
		log.info("Delete " + t);
		Entity<T> entity = schema.getEntity(t);
		if (entity == null) {
			callback.onFailure(new UnknownEntityException(t.getClass()
					.getName()));
			return;
		}
		String key = entity.getEntityName() + "." + entity.getId(t);
		storage.removeItem(key);
		callback.onSuccess(null);
	}
}
